文章内容:用 webpack 打包两个模块(通过 import 语法导入模块),分析打包后的代码,也就是 webpack 运行时代码。

# 代码准备

index.js

// const sum = require('./sum')
import sum, { test } from "./sum";
import * as s from "./sum";

console.log(sum(6, 9));
console.log("test", test);
console.log("s", s);
1
2
3
4
5
6
7

sum.js

const sum = (a, b) => {
  return a + b;
};

export default sum;
export const test = "test";
1
2
3
4
5
6

webpack.config.js

const path = require("path");
module.exports = {
  entry: "./index.js",
  output: {
    path: path.resolve(__dirname, "build"),
  },
  mode: "none",
};
1
2
3
4
5
6
7
8

在控制台执行

npx webpack
1

输出 build/main.js 文件

/******/ (() => {
  // webpackBootstrap
  /******/ "use strict";
  /******/ var __webpack_modules__ = [
    ,
    /* 0 */ /* 1 */
    /***/ (
      __unused_webpack_module,
      __webpack_exports__,
      __webpack_require__
    ) => {
      __webpack_require__.r(__webpack_exports__);
      /* harmony export */ __webpack_require__.d(__webpack_exports__, {
        /* harmony export */ default: () => __WEBPACK_DEFAULT_EXPORT__,
        /* harmony export */ test: () => /* binding */ test,
        /* harmony export */
      });
      const sum = (a, b) => {
        return a + b;
      };

      /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = sum;
      const test = "test";

      /***/
    },
    /******/
  ];
  /************************************************************************/
  /******/ // The module cache
  /******/ var __webpack_module_cache__ = {};
  /******/
  /******/ // The require function
  /******/ function __webpack_require__(moduleId) {
    /******/ // Check if module is in cache
    /******/ var cachedModule = __webpack_module_cache__[moduleId];
    /******/ if (cachedModule !== undefined) {
      /******/ return cachedModule.exports;
      /******/
    }
    /******/ // Create a new module (and put it into the cache)
    /******/ var module = (__webpack_module_cache__[moduleId] = {
      /******/ // no module.id needed
      /******/ // no module.loaded needed
      /******/ exports: {},
      /******/
    });
    /******/
    /******/ // Execute the module function
    /******/ __webpack_modules__[moduleId](
      module,
      module.exports,
      __webpack_require__
    );
    /******/
    /******/ // Return the exports of the module
    /******/ return module.exports;
    /******/
  }
  /******/
  /************************************************************************/
  /******/ /* webpack/runtime/define property getters */
  /******/ (() => {
    /******/ // define getter functions for harmony exports
    /******/ __webpack_require__.d = (exports, definition) => {
      /******/ for (var key in definition) {
        /******/ if (
          __webpack_require__.o(definition, key) &&
          !__webpack_require__.o(exports, key)
        ) {
          /******/ Object.defineProperty(exports, key, {
            enumerable: true,
            get: definition[key],
          });
          /******/
        }
        /******/
      }
      /******/
    };
    /******/
  })();
  /******/
  /******/ /* webpack/runtime/hasOwnProperty shorthand */
  /******/ (() => {
    /******/ __webpack_require__.o = (obj, prop) =>
      Object.prototype.hasOwnProperty.call(obj, prop);
    /******/
  })();
  /******/
  /******/ /* webpack/runtime/make namespace object */
  /******/ (() => {
    /******/ // define __esModule on exports
    /******/ __webpack_require__.r = (exports) => {
      /******/ if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
        /******/ Object.defineProperty(exports, Symbol.toStringTag, {
          value: "Module",
        });
        /******/
      }
      /******/ Object.defineProperty(exports, "__esModule", { value: true });
      /******/
    };
    /******/
  })();
  /******/
  /************************************************************************/
  var __webpack_exports__ = {};
  // This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
  (() => {
    __webpack_require__.r(__webpack_exports__);
    /* harmony import */ var _sum__WEBPACK_IMPORTED_MODULE_0__ =
      __webpack_require__(1);

    console.log((0, _sum__WEBPACK_IMPORTED_MODULE_0__["default"])(6, 9));
    console.log("test", _sum__WEBPACK_IMPORTED_MODULE_0__.test);
    console.log("s", _sum__WEBPACK_IMPORTED_MODULE_0__);
  })();

  /******/
})();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121

然后就可以直接断点调试 build/main.js 啦~

# 运行时代码分析

首先与不包含 ESM 的产物代码(这里用的是《分析一个极简的 Webpack 运行时代码 (opens new window)》文章中的产物代码)做对比

标红的部分是 webpack 为处理 ESM 模块所增加的处理逻辑

# 分析变量与函数

  • __webpack_modules__: 是一个数组,存放所有加载到的模块。
  • __webpack_module_cache__: 是一个对象,对模块执行结果进行缓存,这样能够保证每个模块只被执行一次;
  • __webpack_require__: 是一个函数,作用是加载模块,如果模块第一次被加载,则通过 __webpack_modules__[moduleId] 匹配上对应的模块,并进行缓存;如果是已加载的模块,则直接从 __webpack_module_cache__[moduleId] 取;
  • __webpack_require__.d: 定义 getter 方法
  • __webpack_require__.o: Object.prototype.hasOwnProperty
  • __webpack_require__.r: 注入 ESM 标注属性
  • __webpack_exports__: 是一个对象,存放导出的内容;

# __webpack_require__.r

注入 ESM 标注属性

/******/ /* webpack/runtime/make namespace object */
/******/ (() => {
  /******/ // define __esModule on exports
  /******/ __webpack_require__.r = (exports) => {
    /******/ if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
      /******/ Object.defineProperty(exports, Symbol.toStringTag, {
        value: "Module",
      });
      /******/
    }
    /******/ Object.defineProperty(exports, "__esModule", { value: true });
    /******/
  };
  /******/
})();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

代码解析:

  1. 在函数体内部,首先通过检查环境中是否存在 Symbol 并且 Symbol.toStringTag 是否可用,来判断是否可以使用 toStringTag 属性。这一部分代码可以说是对环境的兼容性检测。
  2. 如果环境中存在 Symbol 并且 Symbol.toStringTag 可用,那么就会调用 Object.defineProperty 方法,在 exports 对象上定义 Symbol.toStringTag 属性,属性值为 'Module'这样做可以使得该模块在被打印或转换为字符串时能够显示为 '[object Module]'
  3. 不管上述条件是否成立,都会调用 Object.defineProperty 方法,在 exports 对象上定义 __esModule 属性,属性值为 true。这个属性是为了表示该模块是一个 ES6 模块,并且在其他模块引入时可以进行相应的处理。

Symbol.toStringTag

Symbol.toStringTag 是一个内置 symbol,它通常作为对象的属性键使用,对应的属性值应该为字符串类型,这个字符串用来表示该对象的自定义类型标签,通常只有内置的 Object.prototype.toString() 方法会去读取这个标签并把它包含在自己的返回值里。

引自 MDN-Symbol.toStringTag (opens new window)

Object.prototype.toString.call(exports) // '[object Module]'

# __webpack_require__.d

/******/ 	/* webpack/runtime/define property getters */
/******/ 	(() => {
/******/ 		// define getter functions for harmony exports
//
/******/ 		__webpack_require__.d = (exports, definition) => {
/******/ 			for(var key in definition) {
/******/ 				if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ 					Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ 				}
/******/ 			}
/******/ 		};
/******/ 	})();
  • 定义 getter 方法:遍历 definitionkey,如果 definition 有这个 key 而 exports 没有,则在 exports 的属性 key 上挂载 definition[key] 这个 getter 方法;
  • getter 方法的目的是:在访问某个导出特性的时候才去计算对应的值;
  • 这里的 definition 是传入的对象:
{
  "default": () => (__WEBPACK_DEFAULT_EXPORT__),
  "test": () => (/* binding */ test)
}

# sum.js 源码与 webpack 运行时代码做对比分析

sum.js 源码

const sum = (a, b) => {
  return a + b;
}

export default sum;
export const test = 'test';

webpack 运行时代码中的 sum 模块

/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "default": () => (__WEBPACK_DEFAULT_EXPORT__),
/* harmony export */   "test": () => (/* binding */ test)
/* harmony export */ });
const sum = (a, b) => {
  return a + b;
}

/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (sum);
const test = 'test';


/***/ })
/******/ 	])

通过 __webpack_require__.r 标记 __webpack_exports__为 ESM 模块,然后通过 __webpack_require__.d__webpack_exports__defaulttest 导出特性定义 getter 方法。

# 对整个过程进行分析

var __webpack_exports__ = {};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
(() => {
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _sum__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);

console.log((0,_sum__WEBPACK_IMPORTED_MODULE_0__["default"])(6, 9));
console.log("test", _sum__WEBPACK_IMPORTED_MODULE_0__.test);
console.log("s", _sum__WEBPACK_IMPORTED_MODULE_0__)
})();
  • 加载入口模块 index.js;

__webpack_require__.r(__webpack_exports__);

  • 将 index.js 标记为 ESM


var _sum__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);

  • 加载 sum 模块

    • 如果缓存中存在,则在缓存中取
    • 将 sum 模块标记为 ESM
    • 为 sum 模块的导出特性定义 getter 方法
  • 将结果赋值给 _sum__WEBPACK_IMPORTED_MODULE_0__


console.log((0,_sum__WEBPACK_IMPORTED_MODULE_0__["default"])(6, 9));
console.log("test", _sum__WEBPACK_IMPORTED_MODULE_0__.test);
  • 调用 default 对应的 getter 方法
  • 调用 test 对应的 getter 方法

# 小结

本文主要对 webpack 打包含有 ESM 模块的运行时代码进行了分析,主要做了如下一些事情:

  • 用 webpack 提供的模块加载函数加载入口模块;

  • 加载入口模块所依赖的模块;

    • 如果缓存中存在,则在缓存中取
    • 将 import 导入的模块标记为 ESM(__esModule
    • 为模块导出特性定义 getter 方法
  • 运行模块中的代码。

上次更新: 2024年3月10日星期日上午10点19分